package logger

import (
	"context"
	"fmt"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/go-kratos/kratos/v2/log"
	"github.com/go-kratos/kratos/v2/middleware"
	"github.com/go-kratos/kratos/v2/transport"
)

const (
	DefaultMaxLogResponseSize = 1024
	EnvLogEnabled             = "LOG_ENABLED"
	EnvMaxLogResponseSize     = "MAX_LOG_RESPONSE_SIZE"
)

type serverOptions struct {
	logEnabled         bool
	maxLogResponseSize int
}

var (
	cachedOptions serverOptions
	once          sync.Once
)

// initializeOptions caches environment variable values to reduce overhead
func initializeOptions() {
	cachedOptions = serverOptions{
		logEnabled:         getEnvBool(EnvLogEnabled, true),
		maxLogResponseSize: getEnvInt(EnvMaxLogResponseSize, DefaultMaxLogResponseSize),
	}
}

func getEnvBool(key string, defaultValue bool) bool {
	if val, ok := os.LookupEnv(key); ok {
		switch valLower := strings.ToLower(val); valLower {
		case "true", "1", "yes", "on":
			return true
		case "false", "0", "no", "off":
			return false
		}
	}
	return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
	if val, ok := os.LookupEnv(key); ok {
		if parsed, err := strconv.Atoi(val); err == nil {
			return parsed
		}
	}
	return defaultValue
}

// Server creates a server logging middleware
// Attention:
// 1. EnvLogEnabled if not set, the default value is true, false will not wire the log
// 2. EnvMaxLogResponseSize if not set, the default value is 1024,0 will not wire the response
func Server(logger log.Logger) middleware.Middleware {
	once.Do(initializeOptions)
	return func(handler middleware.Handler) middleware.Handler {
		return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
			if !cachedOptions.logEnabled {
				return handler(ctx, req)
			}

			startTime := time.Now()
			reply, err = handler(ctx, req)

			logLevel, logFields := prepareLogFields(ctx, req, reply, err, startTime, "server")
			_ = log.WithContext(ctx, logger).Log(logLevel, logFields...)
			return
		}
	}
}

func prepareLogFields(ctx context.Context, req, reply interface{}, err error, startTime time.Time, kind string) (log.Level, []interface{}) {
	fields := []interface{}{
		"kind", kind,
	}

	if info, ok := transport.FromServerContext(ctx); ok {
		fields = append(fields, "component", info.Kind().String(), "operation", info.Operation())
	}

	fields = append(fields, "args", extractArgs(req))

	// It is not ruled out that some errors will also return data
	if cachedOptions.maxLogResponseSize > 0 {
		fields = append(fields, "response", extractResponse(reply, cachedOptions.maxLogResponseSize))
	}

	logLevel := log.LevelInfo
	if err != nil {
		fields = append(fields, "stack", fmt.Sprintf("%+v", err))
		logLevel = log.LevelError
	}

	fields = append(fields, "latency", time.Since(startTime).Seconds())
	return logLevel, fields
}

func extractArgs(req interface{}) string {
	if stringer, ok := req.(fmt.Stringer); ok {
		return stringer.String()
	}
	return fmt.Sprintf("%+v", req)
}

func extractResponse(reply interface{}, maxSize int) string {
	responseStr := fmt.Sprintf("%+v", reply)
	if len(responseStr) > maxSize {
		return fmt.Sprintf("%s... [truncated %d, original size: %d]", responseStr[:maxSize], maxSize, len(responseStr))
	}
	return responseStr
}
